package bancosys.tec.rtm.impl.diagnostician;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import bancosys.tec.rtm.diagnostician.AbstractDiagnostician;
import bancosys.tec.rtm.diagnostician.DiagnosisBean;

/**
 * Diagnosticador que informa, dado um path de um diretório ou de um arquivo, os dados deste arquivo ou de todos os arquivos da árvore deste
 * diretório.
 * 
 * @author Evaristo
 * @version $Id:$ <b>Created on: </b> 29/02/2008
 */
public class ListAndInfoFilesDiagnostician extends AbstractDiagnostician {

    private static final int BYTE_ARRAY_SIZE = 1024;

    private static final int MD5_BYTE_LIMIT = (300 * 1024);

    private static final String HASH_ALGORITHM = "MD5";

    private static final short MASK_FOUR_BITS = 0xf;

    private static final short FOUR_BITS = 4;

    private static final short CHAR_NINE = 9;

    private static final short OFFSET = 10;

    private String path;

    /**
     * Contrutor.
     * 
     * @param path um caminho para um diretório.
     */
    public ListAndInfoFilesDiagnostician(String path) {
        this.path = path;
    }

    /**
     * {@inheritDoc}
     */
    public String getName() {
        return "Dados de arquivos do caminho: " + path;
    }

    /**
     * Método que realiza uma diagnose mostrando todos os dados do arquivo ou de todos os arquivos dentro do diretório do path passado no
     * construtor do diagnosticador. {@inheritDoc}
     */
    @Override
    protected boolean fillDiagnosisBean(DiagnosisBean bean) throws Exception {
        File file = new File(this.path);
        if (file.exists()) {
            bean.addDescription("Listagem dos arquivod do caminho: " + this.path);
            this.diagnosisAllFiles(file, bean);
            return true;
        } else {
            bean.addDescription("O caminho \"" + this.path + "\" passado não existe.");
            return false;
        }
    }

    /**
     * Método para percorrer toda uma árvore de diretórios e diagnosticar os dados de todos os arquivos.
     * 
     * @param file o arquivo ou diretorio inicial.
     * @param diagnosisBean o resultado do diagnóstico.
     */
    private void diagnosisAllFiles(File file, DiagnosisBean diagnosisBean) {
        Map<String, String> visitedDirectories = new HashMap<String, String>();
        this.diagnosisFile(file, diagnosisBean, visitedDirectories);
    }

    /**
     * Método recursivo para percorrer toda uma árvore de diretórios e diagnosticar os dados de todos os arquivos, ignorando os diretórios
     * já visitados que são passados no mapa do parâmetro. O warning se deve a o projeto ter que ser compilado em java 1.4
     * 
     * @param file o arquivo ou diretorio inicial.
     * @param diagnosisBean o resultado do diagnóstico.
     * @param visitedDirectories mapa de diretórios visitados.
     */
    private void diagnosisFile(File file, DiagnosisBean diagnosisBean, Map<String, String> visitedDirectories) {
        if (file.isDirectory() && !visitedDirectories.containsKey(file.getAbsolutePath())) {
            visitedDirectories.put(file.getAbsolutePath(), file.getAbsolutePath());
            diagnosisBean.addDescription("");
            diagnosisBean.addDescription("Diretório: " + file.getPath());
            File[] files = file.listFiles();
            for (int i = 0; i < files.length; i++) {
                this.diagnosisFile(files[i], diagnosisBean, visitedDirectories);
            }
        } else if (file.isFile()) {
            try {
                diagnosisBean.addDescription("  Arquivo: " + file.getName() + " | Tamanho: " + file.length() + " | Data: "
                        + new Date(file.lastModified()) + " | MD5Sum: " + this.md5Sum(file));
            } catch (IOException e) {
                diagnosisBean.addDescription("  Arquivo: " + file.getName() + " | Tamanho: " + file.length() + " | Data: "
                        + new Date(file.lastModified()) + " | MD5Sum: Erro para executar o MD5.");
            }
        }
    }

    // XXX: os métodos abaixo estão replicados aqui, foram retirados do MD5SumTool,
    // isso foi feito pois o BWPAG é compilado com java 1.4 e não pode usar o bancosys-tec-util
    // que é compilado com o 1.5, quando o PAG for compilado com 1.5 esses métodos poderão ser
    // removidos e essa classe deverá usar o MD5SumTool do bancosys-tec-util

    /**
     * Retorna o hash MD5 do arquivo passado no parâmetro. O hash MD5 calculado do primeiro byte até o limite contido na variável estática
     * MD5_BYTE_LIMIT da própria classe.
     * 
     * @param file - Arquivo para ser lido e calculado o hash MD5
     * @return String - String contendo hash md5 do arquivo
     * @throws IOException - Erro na leitura e manipulação do arquivo
     */
    private String md5Sum(File file) throws IOException {
        FileInputStream in = new FileInputStream(file.getAbsolutePath());
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        byte[] byteArrayToWrite = new byte[BYTE_ARRAY_SIZE];
        int numberReadBytes = 0;

        while ((numberReadBytes = in.read(byteArrayToWrite)) != -1) {
            out.write(byteArrayToWrite, 0, numberReadBytes);
            if (out.size() > MD5_BYTE_LIMIT) {
                break;
            }
        }

        try {
            return md5Sum(out.toByteArray(), MD5_BYTE_LIMIT);
        } finally {
            in.close();
            out.close();
        }
    }

    /**
     * Retorna o hash MD5 do array de bytes passado no parâmetro. Os bytes a serem usados no calculo limitado pelo parâmetro limite.
     * 
     * @param input - Array de bytes que será a base para o calculo do hash MD5
     * @param limit - Quantidade de bytes que será usada para o calculo do hash MD5
     * @return String - String contendo hash md5 do array de bytes passado por parâmetro
     */
    public static String md5Sum(byte[] input, int limit) {
        try {
            MessageDigest md = MessageDigest.getInstance(HASH_ALGORITHM);
            md.reset();

            byte[] digest;

            if (limit == -1) {
                digest = md.digest(input);
            } else {
                md.update(input, 0, limit > input.length ? input.length : limit);
                digest = md.digest();
            }

            StringBuffer hexString = new StringBuffer();

            for (int i = 0; i < digest.length; i++) {
                hexString.append(hexDigit(digest[i]));
            }

            return hexString.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException(e.getMessage());
        }
    }

    /**
     * Converte o digito em formato de byte passado por parâmetro para hexadecimal, retornado em formato String.
     * 
     * @param digit - byte a ser convertido para hexadecimal
     * @return String - byte convertido para hexadecimal em formato String
     */
    private static String hexDigit(byte digit) {
        StringBuffer sb = new StringBuffer();
        char character;

        // First nibble
        character = (char) ((digit >> FOUR_BITS) & MASK_FOUR_BITS);
        if (character > CHAR_NINE) {
            character = (char) ((character - OFFSET) + 'a');
        } else {
            character = (char) (character + '0');
        }

        sb.append(character);

        // Second nibble
        character = (char) (digit & MASK_FOUR_BITS);
        if (character > CHAR_NINE) {
            character = (char) ((character - OFFSET) + 'a');
        } else {
            character = (char) (character + '0');
        }

        sb.append(character);
        return sb.toString();
    }
}